package com.thaiopensource.validate.nvdl;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.Vector;

import com.thaiopensource.util.Equal;

/**
 * Keeps modes depending on context. The structure of the context map is stores
 * the mode for / in rootValue "" in otherValue (this is for relative paths)
 * stores a hash with the last path elements as key and ContextMap objects as
 * values. A path like a/b and mode x will be represented by 3 ContextMap
 * objects ContextMap b ---> ContextMap a ---> ContextMap otherValue=x Addind
 * also /a/b and mode y will give ContextMap b ---> ContextMap a ---> ContextMap
 * (otherValue=x, rootValue=y) Adding a2/b and mode w will give ContextMap b
 * ---> ContextMap a ---> ContextMap (otherValue=x, rootValue=y) a2 --->
 * ContextMap otherValue=w
 */
class ContextMap
{
  /**
   * Stores the mode associated with an absolute path.
   */
  private Object rootValue;
  /**
   * Stores a mode associated with a relative path.
   */
  private Object otherValue;
  /**
   * Stores a hash map with with the key the last local name and as values other
   * ContextMap objects.
   */
  private final Hashtable nameTable = new Hashtable ();

  /**
   * Get the mode matching a list of local names. A root more returned means an
   * exact matching of the given local names with the local names from the
   * context map. Otherwise we can get either a mode stored as otherValue or
   * null if the given context does not match any of the stored paths.
   * 
   * @param context
   *        The list of local names that represent a section context (path from
   *        root local element names from the same namespace).
   * @return A mode or null.
   */
  Object get (final Vector context)
  {
    return get (context, context.size ());
  }

  /**
   * Adds a single path (isRoot, names) and a mode to be used for this path =
   * context.
   * 
   * @param isRoot
   *        True if the path starts with /
   * @param names
   *        The local names that form the path.
   * @param value
   *        The mode.
   * @return true if there is no duplicate path, false otherwise.
   */
  boolean put (final boolean isRoot, final Vector names, final Object value)
  {
    return put (isRoot, names, names.size (), value);
  }

  /**
   * Get the mode matching a list of local names. A root more returned means an
   * exact matching of the given local names with the local names from the
   * context map. Otherwise we can get either a mode stored as otherValue or
   * null if the given context does not match any of the stored paths.
   * 
   * @param context
   *        The list of local names that represent a section context (path from
   *        root local element names from the same namespace).
   * @param len
   *        The lenght we should take from the list.
   * @return A mode or null.
   */
  private Object get (final Vector context, final int len)
  {
    if (len > 0)
    {
      final ContextMap nestedMap = (ContextMap) nameTable.get (context.elementAt (len - 1));
      if (nestedMap != null)
      {
        final Object value = nestedMap.get (context, len - 1);
        if (value != null)
          return value;
      }
    }
    if (rootValue != null && len == 0)
      return rootValue;
    return otherValue;
  }

  /**
   * Adds a single path (isRoot, names) and a mode to be used for this path =
   * context.
   * 
   * @param isRoot
   *        True if the path starts with /
   * @param names
   *        The local names that form the path.
   * @param len
   *        The length if the names vector.
   * @param value
   *        The mode.
   * @return true if there is no duplicate path, false otherwise.
   */
  private boolean put (final boolean isRoot, final Vector names, final int len, final Object value)
  {
    if (len == 0)
    {
      // if we have only /
      if (isRoot)
      {
        if (rootValue != null)
          return false;
        rootValue = value;
      }
      // We followed all the paths, it is not root,
      // then we store the mode as the other value.
      else
      {
        if (otherValue != null)
          return false;
        otherValue = value;
      }
      return true;
    }
    else
    {
      // get the last local name from the path
      final Object name = names.elementAt (len - 1);
      // Get the context map mapped in nameTable to that name.
      ContextMap nestedMap = (ContextMap) nameTable.get (name);
      // Not preset then create it.
      if (nestedMap == null)
      {
        nestedMap = new ContextMap ();
        nameTable.put (name, nestedMap);
      }
      // Add the rest of the path names in the nested context map.
      return nestedMap.put (isRoot, names, len - 1, value);
    }
  }

  /**
   * Chek that this context map is equals with a specified context map.
   */
  @Override
  public boolean equals (final Object obj)
  {
    if (!(obj instanceof ContextMap))
      return false;
    final ContextMap other = (ContextMap) obj;
    if (!Equal.equal (this.rootValue, other.rootValue) || !Equal.equal (this.otherValue, other.otherValue))
      return false;
    // We want jing to work with JDK 1.1 so we cannot use Hashtable.equals
    if (this.nameTable.size () != other.nameTable.size ())
      return false;
    for (final Enumeration e = nameTable.keys (); e.hasMoreElements ();)
    {
      final Object key = e.nextElement ();
      if (!nameTable.get (key).equals (other.nameTable.get (key)))
        return false;
    }
    return true;
  }

  /**
   * Get a hashcode for this context map.
   */
  @Override
  public int hashCode ()
  {
    int hc = 0;
    if (rootValue != null)
      hc ^= rootValue.hashCode ();
    if (otherValue != null)
      hc ^= otherValue.hashCode ();
    for (final Enumeration e = nameTable.keys (); e.hasMoreElements ();)
    {
      final Object key = e.nextElement ();
      hc ^= key.hashCode ();
      hc ^= nameTable.get (key).hashCode ();
    }
    return hc;
  }

  /**
   * Creates an Enumeration implementation that enumerates all the modes stored
   * in this context map and in the nested context maps.
   */
  static private class Enumerator implements Enumeration
  {
    /**
     * Store this context map root value.
     */
    private Object rootValue;

    /**
     * Store this context map other value.
     */
    private Object otherValue;

    /**
     * Stores the enumeration of modes of the current subMap.
     */
    private Enumeration subMapValues;

    /**
     * Stores the ContextMap objects from the nameTable.
     */
    private final Enumeration subMaps;

    private Enumerator (final ContextMap map)
    {
      rootValue = map.rootValue;
      otherValue = map.otherValue;
      subMaps = map.nameTable.elements ();
    }

    /**
     * Advance to the next context map values in subMapValues and to the next
     * element in subMap enumeration, if needed.
     */
    private void prep ()
    {
      while ((subMapValues == null || !subMapValues.hasMoreElements ()) && subMaps.hasMoreElements ())
        subMapValues = ((ContextMap) subMaps.nextElement ()).values ();
    }

    /**
     * True if we have more elements.
     */
    public boolean hasMoreElements ()
    {
      prep ();
      return rootValue != null || otherValue != null || (subMapValues != null && subMapValues.hasMoreElements ());
    }

    /**
     * Get the next element (mode in this case).
     */
    public Object nextElement ()
    {
      if (rootValue != null)
      {
        final Object tem = rootValue;
        rootValue = null;
        return tem;
      }
      if (otherValue != null)
      {
        final Object tem = otherValue;
        otherValue = null;
        return tem;
      }
      prep ();
      if (subMapValues == null)
        throw new NoSuchElementException ();
      return subMapValues.nextElement ();
    }
  }

  /**
   * Get an enumeration with all the modes in this context map.
   * 
   * @return An enumeration containing Mode objects.
   */
  Enumeration values ()
  {
    return new Enumerator (this);
  }
}
